package com.jellyfishumbrella.GL;

import static org.lwjgl.opengl.GL11.GL_LINE_LOOP;

import java.util.ArrayList;
import java.util.HashSet;

//import com.jellyfishumbrella.GL.Primitives.Cube;

public class OctreeNode<T extends AbstractOctreeFiller> extends HashSet<T> implements Renderable {
	public static final int	MAX_SUBD_LEVEL	= 1;
	public OctreeNode		parent;
	public OctreeNode[][][]	subNode;

	public int				index_i			= 0;
	public int				index_j			= 0;
	public int				index_k			= 0;

	public Vector3d			bounds[]		= new Vector3d[2];
//	public Cube				displayCube;
	public boolean			isSubdivided	= false;
	public int				subDlevel		= 0;

	public boolean			renderMe		= false;

	public boolean			isComplete		= false;

	// public LinkedBlockingQueue<AnimObject> animatables;
	// public LinkedBlockingQueue<Renderable> renderables;
	// public ArrayList<Polygon> polygons;

	public OctreeNode() {
		super();
	}

	public OctreeNode(OctreeNode parent, Vector3d ltf, Vector3d rbb) {
		super();
		create(parent, ltf, rbb, 0);
	}

	public OctreeNode(OctreeNode parent, Vector3d ltf, Vector3d rbb, int subDlevel) {
		super();
		create(parent, ltf, rbb, subDlevel);
	}

	public void create(OctreeNode parent, Vector3d ltf, Vector3d rbb, int subDlevel) {
		this.parent = parent;
		this.bounds[0] = ltf;
		this.bounds[1] = rbb;
		// animatables = new LinkedBlockingQueue<AnimObject>();
		// renderables = new LinkedBlockingQueue<Renderable>();

		double maxDimension = maxDimension();

//		displayCube = new Cube(bounds[0], bounds[1]);
		//displayCube.keyframes.here.pos = center();
//		displayCube.glMode = GL_LINE_LOOP;

		//this.bounds[0] = new Vector3d(-maxDimension, -maxDimension, -maxDimension);
		//this.bounds[1] = new Vector3d(maxDimension, maxDimension, maxDimension);

		this.subDlevel = subDlevel;

		AbstractGameLogic.renderGL.add(this);
	}

	public double maxDimension() {
		return Math.max(Math.max(width(), height()), depth());
	}

	public void insert(Polygon filler) {
		for (Vertex currVert : filler.verts) {
			if (contains(currVert)) {
				if (subDlevel < MAX_SUBD_LEVEL) {
					if (subNode == null) {
						subdivide();
					}
					Vector3d center = center();
					int i = 0;
					int j = 0;
					int k = 0;
					if (currVert.x() > center.x) {
						i = 1;
					}
					if (currVert.y() < center.y) {
						j = 1;
					}
					if (currVert.z() > center.z) {
						k = 1;
					}
					subNode[i][j][k].add(filler);
				} else {
					// renderables.add(displayCube);
					// animatables.add(obj);
					// if (obj instanceof Renderable) {
					// renderables.add((Renderable) obj);
					// }
					filler.octreeNode.add(this);
				}
			} else {
				if (parent != null) {
					parent.add(filler);
				}
			}
		}
	}

	public void insert(PolygonGroup polyGroup) {
		//System.out.println(subDlevel + "     " + index_i +"   "+ index_j +"   "+ index_k);
		//System.out.println(bounds[0].toString() + " |---------------------| " + bounds[1].toString());
		/*
		if (contains(polyGroup)) {
			add((T) polyGroup);
			polyGroup.octreeNode.add(this);
			if (subDlevel < MAX_SUBD_LEVEL) {
				if (subNode == null) {
					subdivide();
				}
				for (int k = 0; k < 2; k++) {
					for (int j = 0; j < 2; j++) {
						for (int i = 0; i < 2; i++) {
							subNode[i][j][k].insert(polyGroup);
						}
					}
				}
			} else {
				renderMe = true;
			}
		}
		*/
	}

	public void update(AnimObject obj) {
		if (!contains(obj.keyframes.here.pos)) {
			remove(obj);
			if (parent != null) {
				parent.add(obj);
			}
		}
	}

	public void remove(AnimObject obj) {
		// renderables.remove(obj);
		// animatables.remove(obj);
		obj.octreeNode = null;
		if (isEmpty()) {
			boolean allSubNodesEmpty = true;
			if (subNode != null) {
				for (int k = 0; k < 2; k++) {
					for (int j = 0; j < 2; j++) {
						for (int i = 0; i < 2; i++) {
							if (!subNode[i][j][k].isEmpty()) {
								allSubNodesEmpty = false;
								return;
							}
						}
					}
				}
				if (allSubNodesEmpty) {
					subNode = null;
				}
			}
		}
	}

	public boolean contains(Vector3d point) {
		//System.out.println(point.toString());
		//System.out.println(point.x >= bounds[0].x);
		//System.out.println(point.x <= bounds[1].x);
		//System.out.println(point.y >= bounds[0].y);
		//System.out.println(point.y <= bounds[1].y);
		//System.out.println(point.z >= bounds[0].z);
		//System.out.println(point.z <= bounds[1].z);
		if ((point.x >= bounds[0].x && point.x <= bounds[1].x) && (point.y >= bounds[0].y && point.y <= bounds[1].y) && (point.z >= bounds[0].z && point.z <= bounds[1].z)) {
			return true;
		} else {
			return false;
		}
	}

	public void subdivide() {
		this.isSubdivided = true;
		Vector3d center = center();
		subNode = new OctreeNode[2][2][2];

		for (int k = 0; k < 2; k++) {
			for (int j = 0; j < 2; j++) {
				for (int i = 0; i < 2; i++) {
					double X1, Y1, Z1;
					double X2, Y2, Z2;
					if (i == 0) {
						X1 = bounds[0].x;
						X2 = center.x;
					} else {
						X1 = center.x;
						X2 = bounds[1].x;
					}
					if (j == 0) {
						Y1 = bounds[0].y;
						Y2 = center.y;
					} else {
						Y1 = center.y;
						Y2 = bounds[1].y;
					}
					if (k == 0) {
						Z1 = bounds[0].z;
						Z2 = center.z;
					} else {
						Z1 = center.z;
						Z2 = bounds[1].z;
					}
					/*
					subNode[i][j][k] = new OctreeNode<PolygonGroup>(this, new Vector3d(X1, Y1, Z1), new Vector3d(X2, Y2, Z2), subDlevel + 1);
					subNode[i][j][k].index_i = subNode[i][j][k].subDlevel * 2 + i;
					subNode[i][j][k].index_j = subNode[i][j][k].subDlevel * 2 + j;
					subNode[i][j][k].index_k = subNode[i][j][k].subDlevel * 2 + k;
					*/
				}
			}
		}
	}

	public double width() {
		return bounds[1].x - bounds[0].x;
	}

	public double height() {
		return bounds[1].y - bounds[0].y;
	}

	public double depth() {
		return bounds[1].z - bounds[0].z;
	}

	public Vector3d center() {
		return new Vector3d(bounds[0].x + width() / 2.0, bounds[0].y + height() / 2.0, bounds[0].z + depth() / 2.0);
	}

	public boolean doesRayIntersect(Ray ray, double t0, double t1) {//t0 and t1 represent a valid intersection interval
		double tmin, tmax, tymin, tymax, tzmin, tzmax;

		if (ray.direction.x >= 0) {
			tmin = (bounds[0].x - ray.origin.x) / ray.direction.x;
			tmax = (bounds[1].x - ray.origin.x) / ray.direction.x;
		} else {
			tmin = (bounds[1].x - ray.origin.x) / ray.direction.x;
			tmax = (bounds[0].x - ray.origin.x) / ray.direction.x;
		}
		if (ray.direction.y >= 0) {
			tymin = (bounds[0].y - ray.origin.y) / ray.direction.y;
			tymax = (bounds[1].y - ray.origin.y) / ray.direction.y;
		} else {
			tymin = (bounds[1].y - ray.origin.y) / ray.direction.y;
			tymax = (bounds[0].y - ray.origin.y) / ray.direction.y;
		}
		if ((tmin > tymax) || (tymin > tmax))
			return false;

		if (tmin != tmin || tymin > tmin) // i.e tmin == NaN
			tmin = tymin;
		if (tmax != tmax || tymax < tmax) // i.e tmax == NaN
			tmax = tymax;

		if (ray.direction.z >= 0) {
			tzmin = (bounds[0].z - ray.origin.z) / ray.direction.z;
			tzmax = (bounds[1].z - ray.origin.z) / ray.direction.z;
		} else {
			tzmin = (bounds[1].z - ray.origin.z) / ray.direction.z;
			tzmax = (bounds[0].z - ray.origin.z) / ray.direction.z;
		}

		if ((tmin > tzmax) || (tzmin > tmax))
			return false;
		if (tzmin > tmin)
			tmin = tzmin;
		if (tzmax < tmax)
			tmax = tzmax;
		return ((tmin < t1) && (tmax > t0));
	}

	/*
	 * This one is supposedly highly optimized, but I'm not sure it works with java math...
	 * http://www.cs.utah.edu/~awilliam/box/
	 */
	public boolean doesRayIntersect2(Ray ray, double t0, double t1) {//t0 and t1 represent a valid intersection interval
		double tmin, tmax, tymin, tymax, tzmin, tzmax;

		tmin = (bounds[ray.sign[0]].x - ray.origin.x) * ray.invDirection.x;
		tmax = (bounds[1 - ray.sign[0]].x - ray.origin.x) * ray.invDirection.x;
		tymin = (bounds[ray.sign[1]].y - ray.origin.y) * ray.invDirection.y;
		tymax = (bounds[1 - ray.sign[1]].y - ray.origin.y) * ray.invDirection.y;
		if ((tmin > tymax) || (tymin > tmax)) {
			return false;
		}
		if (tymin > tmin) {
			tmin = tymin;
		}
		if (tymax < tmax) {
			tmax = tymax;
		}
		tzmin = (bounds[ray.sign[2]].z - ray.origin.z) * ray.invDirection.z;
		tzmax = (bounds[1 - ray.sign[2]].z - ray.origin.z) * ray.invDirection.z;
		if ((tmin > tzmax) || (tzmin > tmax)) {
			return false;
		}
		if (tzmin > tmin) {
			tmin = tzmin;
		}
		if (tzmax < tmax) {
			tzmax = tmax;
		}
		return ((tmin < t1) && (tmax > t0));
	}

	public boolean contains(Polygon poly) {
		for (Vertex currVert : poly.verts) {
			if (contains(currVert)) {
				return true;
			}
		}
		//System.out.println(poly.verts.size());
		int startIndex;
		if (poly.isPolygonFan) {
			startIndex = 1;
		} else {
			startIndex = 0;
		}
		for (int i = startIndex; i < poly.verts.size(); i++) {
			int wrapIndex = (i + 1);
			if (wrapIndex == poly.verts.size()) {
				wrapIndex = startIndex;
			}
			Vector3d dir = poly.verts.get(wrapIndex).coord.clone();
			double dist = poly.verts.get(i).coord.distance(dir);

			dir.sub(poly.verts.get(i).coord);
			dir.normalize();

			Ray edge = new Ray(poly.verts.get(i).coord, dir);

			if (doesRayIntersect(edge, 0, dist)) {
				return true;
			}
		}
		Ray edge0 = new Ray(bounds[0], new Vector3d(0, 0, 1));
		Ray edge1 = new Ray(bounds[0], new Vector3d(0, 1, 0));
		Ray edge2 = new Ray(bounds[0], new Vector3d(1, 0, 0));
		Ray edge3 = new Ray(bounds[1], new Vector3d(0, 0, -1));
		Ray edge4 = new Ray(bounds[1], new Vector3d(0, -1, 0));
		Ray edge5 = new Ray(bounds[1], new Vector3d(-1, 0, 0));

		if (poly.doesRayIntersect(edge0, 0, width())) {
			return true;
		}

		if (poly.doesRayIntersect(edge1, 0, height())) {
			return true;
		}
		if (poly.doesRayIntersect(edge2, 0, depth())) {
			return true;
		}
		if (poly.doesRayIntersect(edge3, 0, width())) {
			return true;
		}
		if (poly.doesRayIntersect(edge4, 0, height())) {
			return true;
		}
		if (poly.doesRayIntersect(edge5, 0, depth())) {
			return true;
		}

		return false;
	}

	public boolean contains(PolygonGroup polyGroup) {
		for (Polygon currPoly : polyGroup.polygons) {
			if (contains(currPoly)) {
				return true;
			}
		}
		return false;
	}

	public ArrayList<OctreeNode> getIntersectedNodes(Ray ray, double t0, double t1) {
		ArrayList<OctreeNode> nodes = new ArrayList<OctreeNode>();
		//System.out.println(ray.origin.toString() + "   " + this.subDlevel + "     " + bounds[0].toString() + "   " + bounds[1].toString());

		if (doesRayIntersect(ray, t0, t1)) {
			if (this.isSubdivided) {
				for (int k = 0; k < 2; k++) {
					for (int j = 0; j < 2; j++) {
						for (int i = 0; i < 2; i++) {
							nodes.addAll(this.subNode[i][j][k].getIntersectedNodes(ray, t0, t1));
						}
					}
				}
			} else {
				//
				// remove the enclosed statements from the else clause to return ALL octreeNodes, not just the bottom most level
				//
				//System.out.println("ADD: " + this.subDlevel + "     " + bounds[0].toString() + "   " + bounds[1].toString());
				nodes.add(this);
			}
		}

		return nodes;
	}

	@Override
	public void render() {
		if (!isEmpty()) {
			if (isSubdivided) {
				for (int k = 0; k < 2; k++) {
					for (int j = 0; j < 2; j++) {
						for (int i = 0; i < 2; i++) {
							if (this.subNode[i][j][k] != null) {
								this.subNode[i][j][k].render();
							}
						}
					}
				}
			} else if (renderMe) {
//				displayCube.render();
			}
		}

	}
}
